using System;
using System.Collections.Generic;
using System.Text;

namespace GenesisCore
{
	public partial class Genesis
	{
		private const int	CLOCK_SPEED_NTSC = 53693175;
		private const int	LINE_CYCLES = (int)((((double)CLOCK_SPEED_NTSC / 7.0) / 30.0) / 262.0 + 0.5);
		private const int	VISIBLE_LINES = 224;
		private const int	SCREEN_LINES = 264;
		private const int	SCREEN_WIDTH = 320;
		private const int	SCREEN_HEIGHT = 224;

		private uint[]		m_FrameBuffer;
		private uint[]		m_Palette;

		public uint[] FrameBuffer
		{
			get { return this.m_FrameBuffer; }
		}

		public void UpdateFrame()
		{
			// TODO This palette should be calculated on every line (some games change
			// the palette mid-frame to give the illusion of having more colours)
			CalcPalette();

			int[] lineData = new int[320];
			int line;
			for (line = 0; line < VISIBLE_LINES; line++)
			{
				this.m_VdpSR |= 0x0004; // HBlank
				this.m_CPU.Execute(LINE_CYCLES - 404);
				this.m_VdpSR &= 0xFFFB;

				// Fill the linedata buffer
				this.RenderLine(line, lineData);

				// Now convert this to uint Bgr32 format
				int fx = (line << 8) + (line << 6); // fx = i * 320
				for (int x = 0; x < 320; x++)
				{
					this.m_FrameBuffer[fx++] = this.m_Palette[lineData[x]];
				}

				this.m_CPU.Execute(LINE_CYCLES);
			}

			this.m_VdpSR |= 0x0008; // VBlank
			for (; line < SCREEN_LINES; line++)
			{
				this.m_VdpSR |= 0x0004; // HBlank
				this.m_CPU.Execute(LINE_CYCLES - 404);
				this.m_VdpSR &= 0xFFFB;

				this.m_CPU.Execute(LINE_CYCLES);
			}
			this.m_VdpSR &= 0xFFF7;

			// Palette
			//for (int p = 0; p < 4; p++)
			//{
			//    for (int c = 0; c < 16; c++)
			//    {
			//        for (int x = p * 10; x < (p + 1) * 10; x++)
			//        {
			//            for (int y = c * 14; y < (c + 1) * 14; y++)
			//            {
			//                int colour = (p << 4) | c;
			//                this.m_FrameBuffer[y * 320 + x] = this.m_Palette[colour];
			//            }
			//        }
			//    }
			//}

			if ((this.m_VdpRegisters[1] & 32) > 0)
			{
				this.m_CPU.Interrupt(6);
			}
		}

		// Priority bits:
		// 0 - Scroll A
		// 1 - Scroll B
		// 2 - Sprite
		private void RenderLine(int line, int[] lineData)
		{
			int[] spriteData = new int[320];
			int[] layerAData = new int[320];
			int[] layerBData = new int[320];
			int[] priorityData = new int[320];

			int backgroundColour = this.m_VdpRegisters[7] & 0x3F;

			//Sprites(line, spriteData, priorityData);
			Layer(line, layerAData, priorityData, 0);
			Layer(line, layerBData, priorityData, 1);

			// Copy into framebuffer
			for (int i = 0; i < 320; i++)
			{
				// TODO: Need to take into account the highlight/shadow switch
				switch (priorityData[i])
				{
					case 0:
						if ((spriteData[i] & 0x0F) > 0) lineData[i] = spriteData[i];
						else if ((layerAData[i] & 0x0F) > 0) lineData[i] = layerAData[i];
						else if ((layerBData[i] & 0x0F) > 0) lineData[i] = layerBData[i];
						else lineData[i] = backgroundColour;
						break;
					case 1:
						if ((layerAData[i] & 0x0F) > 0) lineData[i] = layerAData[i];
						else if ((spriteData[i] & 0x0F) > 0) lineData[i] = spriteData[i];
						else if ((layerBData[i] & 0x0F) > 0) lineData[i] = layerBData[i];
						else lineData[i] = backgroundColour;
						break;
					case 2:
						if ((layerBData[i] & 0x0F) > 0) lineData[i] = layerBData[i];
						else if ((spriteData[i] & 0x0F) > 0) lineData[i] = spriteData[i];
						else if ((layerAData[i] & 0x0F) > 0) lineData[i] = layerAData[i];
						else lineData[i] = backgroundColour;
						break;
					case 3:
						if ((layerAData[i] & 0x0F) > 0) lineData[i] = layerAData[i];
						else if ((layerBData[i] & 0x0F) > 0) lineData[i] = layerBData[i];
						else if ((spriteData[i] & 0x0F) > 0) lineData[i] = spriteData[i];
						else lineData[i] = backgroundColour;
						break;
					default:
						break;
				}
			}
		}

		private void Layer(int line, int[] layerData, int[] priorityData, int layer) // layer A=0, B=1
		{
			int nameTableAddr = (layer == 0) ?
				(this.m_VdpRegisters[2] & (7 << 3)) << 10 :
				(this.m_VdpRegisters[4] & 7) << 13;

			int hSize = this.m_VdpRegisters[16] & 3;
			int hSizeCells = (hSize + 1) << 5;
			int hSizePixels = hSizeCells << 3;
			//int vsize = (((this.m_VdpRegisters[16] >> 4) & 3) + 1) << 5;

			// Get hscroll offset
			int hScrollEntryAddr = (this.m_VdpRegisters[13] & 0x3F) << 10; // Base address
			switch (this.m_VdpRegisters[11] & 3)
			{
				case 0: // Full screen
					hScrollEntryAddr += layer << 1;
					break;
				case 1: // First cell
					hScrollEntryAddr += ((line & 7) + layer) << 1;
					break;
				case 2: // Every cell
					hScrollEntryAddr += ((line & ~7) + layer) << 1;
					break;
				case 3: // Every line
					hScrollEntryAddr += (line + layer) << 1;
					break;
			}
			int hScrollValue = ((this.m_VdpVideoRam[hScrollEntryAddr] & 3) << 8) | this.m_VdpVideoRam[hScrollEntryAddr + 1];
			int hPixelOffset = (1024 - hScrollValue) & (hSizePixels - 1);
			int hCellOffset = hPixelOffset >> 3;
			int hCellPixelOffset = hPixelOffset & 7;

			int cellStartAddr = nameTableAddr + ((line >> 3) << hSizeMultipliers[hSize]);
			int cellEndAddr = nameTableAddr + (((line + 8) >> 3) << hSizeMultipliers[hSize]);
			int cellAddr = cellStartAddr + (hCellOffset << 1);

			int px = 0;
			for (int i = 0; i < 41; i++)
			{
				// Read hflip & vflip settings
				int vFlip = this.m_VdpVideoRam[cellAddr] & (1 << 4);
				int hFlip = this.m_VdpVideoRam[cellAddr] & (1 << 3);

				// Read palette & priority data
				int palette = (this.m_VdpVideoRam[cellAddr] & (3 << 5)) >> 1;
				int priority = ((this.m_VdpVideoRam[cellAddr] >> 15) & 1) << layer;

				// Pattern information
				int patternAddr = ((this.m_VdpVideoRam[cellAddr] & 7) << 13) | (this.m_VdpVideoRam[cellAddr + 1] << 5);

				// Take into account vflip
				patternAddr += (vFlip == 0) ? (line & 0x7) << 2 : (~line & 0x7) << 2;

				// Load row of patten data into a single integer
				int patternData = (this.m_VdpVideoRam[patternAddr] << 24) |
					(this.m_VdpVideoRam[patternAddr + 1] << 16) |
					(this.m_VdpVideoRam[patternAddr + 2] << 8) |
					this.m_VdpVideoRam[patternAddr + 3];

				// Decide which hflip shift table we're using
				int[] shiftLookupTable = (hFlip == 0) ? hScrollShiftTable : hScrollShiftTableFlip;

				// The main loop - one iteration for every pixel on the screen
				for (; (hCellPixelOffset < 8) && (px < 320); hCellPixelOffset++)
				{
					// Pattern data
					layerData[px] = (patternData >> shiftLookupTable[hCellPixelOffset]) & 0xF;

					// Palette / priority data
					layerData[px] |= palette;
					priorityData[px] |= priority;
					px++;
				}

				hCellPixelOffset = 0;
				cellAddr += 2;
				if (cellAddr == cellEndAddr)
				{
					cellAddr = cellStartAddr;
				}
			}
		}

		private int[] hScrollShiftTable = new int[] { 28, 24, 20, 16, 12, 8, 4, 0 };
		private int[] hScrollShiftTableFlip = new int[] { 0, 4, 8, 12, 16, 20, 24, 28 };
		private int[] hSizeMultipliers = new int[] { 6, 7, 0, 8 };

		private void Sprites(int line, int[] layerData, int[] priorityData)
		{
			int baseSpriteAddress = (this.m_VdpRegisters[5] & 127) << 8;
			int link = 0;

			do
			{
				int spriteAddress = baseSpriteAddress + (link << 3);
				link = this.m_VdpVideoRam[spriteAddress + 3] & 127;

				int ycoord = ((this.m_VdpVideoRam[spriteAddress] & 3) << 8) | this.m_VdpVideoRam[spriteAddress + 1];
				int ycells = (this.m_VdpVideoRam[spriteAddress + 2] & 3) + 1;
				if ((ycoord + (ycells << 3) < 128) || (ycoord > 128 + 224))
					continue;

				int xcoord = ((this.m_VdpVideoRam[spriteAddress + 6] & 3) << 8) | this.m_VdpVideoRam[spriteAddress + 7];
				int xcells = ((this.m_VdpVideoRam[spriteAddress + 2] >> 2) & 3) + 1;
				if ((xcoord + (xcells << 3) < 128) || (xcoord > 128 + 320))
					continue;

				int vFlip = this.m_VdpVideoRam[spriteAddress + 4] & (1 << 4);
				int hFlip = this.m_VdpVideoRam[spriteAddress + 4] & (1 << 3);

				int patternAddr = ((this.m_VdpVideoRam[spriteAddress + 4] & 7) << 13) | (this.m_VdpVideoRam[spriteAddress + 5] << 5);

			} while (link != 0);
		}

		private void CalcPalette()
		{
			for (int i = 0; i < 128; i += 2)
			{
				int blue = (this.m_VdpColourRam[i] >> 1) & 7;
				int green = (this.m_VdpColourRam[i + 1] >> 5) & 7;
				int red = (this.m_VdpColourRam[i + 1] >> 1) & 7;

				int rgb = (red << 16) | (green << 8) | blue;

				// Multiplying by 36 is slow, so do two shifts and an addition
				this.m_Palette[i >> 1] = (uint)((0xFF << 24) | ((rgb << 5) + (rgb << 2)));
			}
		}
	}
}
